const path = require('path')
const async = require('neo-async')
const ejs = require('ejs')
const { Tapable, SyncHook } = require('tapable')
const NormalModuleFactory = require('./NormalModuleFactory')
const Parser = require('./Parser')
const Chunk = require('./Chunk')

// 实例化一个 normalModuleFactory parser
const normalModuleFactory = new NormalModuleFactory()
const parser = new Parser()
class Compilation extends Tapable {
  constructor (compiler) {
    super()
    this.compiler = compiler
    this.context = compiler.context
    this.options = compiler.options
    // 让 compilation 具备文件的读写能力
    this.inputFileSystem = compiler.inputFileSystem
    this.outputFileSystem = compiler.outputFileSystem
    this.entries = [] // 存入所有入口模块的数组
    this.modules = [] // 存放所有模块的数据
    this.chunks = [] // 存放当前次打包过程中所产生出的chunk
    this.assets = []
    this.files = []
    this.hooks = {
      seal: new SyncHook(),
      successModule: new SyncHook(['module']),
      beforeChunk: new SyncHook(),
      afterChunk: new SyncHook()
    }
  }

  /**
     * 完成模块的编译操作
     * @param {*} context 当前项目的跟
     * @param {*} entry 当前的入口文件的相对路径
     * @param {*} name chunkName -> main
     * @param {*} callback 回调函数
     */
  addEntry (context, entry, name, callback) {
    console.log('addEntry')
    this._addModuleChain(context, entry, name, (err, module) => {
      console.log('_addModuleChain 执行了～～')
      callback(err, module)
    })
  }

  _addModuleChain (context, entry, name, callback) {
    const resource = path.posix.join(context, entry) // 当前操作的核心作用就是返回 entry 入口的绝对路径
    this.createModule(
      {
        name,
        context,
        rawRequest: entry,
        resource,
        parser,
        moduleId: './' + path.posix.relative(context, resource)
      },
      (entryModule) => { this.entries.push(entryModule) },
      callback
    )
  }

  /**
     * 定义一个创建模块的方法， 达到复用的目的
     * @param {*} data // 创建模块时所需要的一些属性值
     * @param {*} doAddEntry // 可选参数，在加载入口模块的时候，将入口模块的id 写入 this.entries
     * @param {*} callback
     */
  createModule (data, doAddEntry, callback) {
    const module = normalModuleFactory.create(data)

    const afterBuild = (err, module) => {
      // 在 afterBuild 当中我们就需要判断一些，当前次module 加载完成之后是否需要处理依赖加载
      if (module.dependencies.length > 0) {
        // 当前逻辑就是表示module 有需要加载的模块， 因此我们可以再单独定一个方法来实现
        this.processDependencies(module, err => {
          callback(err, module)
        })
      } else {
        callback(err, module)
      }
    }

    this.buildModule(module, afterBuild)

    // 当我们完成了本次的 build 操作之后将 module 进行保存
    doAddEntry && doAddEntry(module)
    this.modules.push(module)
  }

  /**
     * 完成具体的 build 行为
     * @param {*} module 当前需要被编译的模块
     * @param {*} callback
     */
  buildModule (module, callback) {
    module.build(this, err => {
      // 如果代码走到这里就意味着当前 Module 的编译完成
      this.hooks.successModule.call(module)
      callback(err, module)
    })
  }

  processDependencies (module, callback) {
    // 01 当前的函数核心功能就是实现一个被依赖模块的递归加载
    // 02 加载模块的思想都是创建一个模块，然后想办法将被加载模块的内容拿进来
    // 03 当前我们不知道 module 需要依赖几个模块， 此时我们需要想办法让所有的被依赖的模块都加载完成之后再执行 callback [neo-async]
    const dependencies = module.dependencies
    async.forEach(dependencies, (dependcy, done) => {
      this.createModule({
        parser,
        name: dependcy.name,
        context: dependcy.context,
        rawRequest: dependcy.rawRequest,
        moduleId: dependcy.moduleId,
        resource: dependcy.resource
      }, null, done)
    }, callback)
  }

  seal (callback) {
    this.hooks.seal.call()
    this.hooks.beforeChunk.call()

    // 01 当前所有的入口模块都被存放在 compilation 对象的 entries 数组中
    // 02 所谓封装 chunk 指的是依据某个入口，然后找到它所有的依赖，将他们的源代码放在一起， 之后再做合并

    for (const entryModule of this.entries) {
      const chunk = new Chunk(entryModule)

      // 保存 chunk 信息
      this.chunks.push(chunk)

      // 给 chunk 属性赋值
      chunk.modules = this.modules.filter(module => module.name === chunk.name)
    }

    // chunk 流程梳理之后就进入到了 chunk 代码处理环节（模版文件 + 模版中的源代码 ==> chunk.js）
    this.hooks.afterChunk.call(this.chunks)

    // 生成代码内容
    this.createChunkAssets()

    callback()
  }

  /**
   * 渲染 chunk 的 ast 树
   */
  createChunkAssets () {
    for (let i = 0; i < this.chunks.length; i++) {
      const chunk = this.chunks[i]
      const fileName = chunk.name + '.js'
      chunk.files.push(fileName)

      // 01 获取模版文件的路径
      const tempPath = path.posix.join(__dirname, 'temp/main.ejs')
      // 02 读取模块文件中的内容
      const tempCode = this.inputFileSystem.readFileSync(tempPath, 'utf8')
      // 03 获取渲染函数
      const tempRender = ejs.compile(tempCode)
      // 04 按ejs的语法渲染数据
      const source = tempRender({
        entryModuleId: chunk.entryModule.moduleId,
        modules: chunk.modules
      })

      // 输出文件内容
      this.emitAssets(fileName, source)
    }
  }

  emitAssets (fileName, source) {
    this.assets[fileName] = source
    this.files.push(fileName)
  }
}
module.exports = Compilation
